"""
====================================================================
Copyright (C) 2012-2013 Eric Blond

This file is part of Neptune.

Neptune is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Neptune is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Neptune.  If not, see <http://www.gnu.org/licenses/>.
====================================================================

Created on Jun 8, 2012

@author: Eric
"""

import node
import inspect
import copy
from . import thread
from . import exception
from . import type_mgr

#######

def is_noarg_method(function):
    args, vargs, kwargs, defaults = inspect.getargspec(function)
    return (len(args) == 1) and (vargs is None) \
       and (kwargs is None) and (defaults is None)

#######

class CellOptionEnum(object):
    
    SETTABLE  = 'settable'
    TWEAKS    = 'tweaks'
    ON_CHANGE = 'on_change'
    TYPE      = 'type'
    ON_SET    = 'on_set'

#######

def function_name(value):
    if isinstance(value, str):
        return value
    elif hasattr(value, '__name__'):
        return value.__name__

#######
            
def cell(*args, **kwargs):
    if (len(args) == 1) and inspect.isfunction(args[0]):
        function = args[0]
        if not is_noarg_method(function):
            raise exception.DefinitionException('Cell "%s" cannot have parameters' % function.__name__)
        function._NEPTUNE_declaration = {}
        return function
    else:
        if args:
            raise exception.DefinitionException('All cell arguments should be named')
        declaration = {}
        for (k, v) in kwargs.items():
            correct = False
            if k == CellOptionEnum.SETTABLE:
                if isinstance(v, bool):
                    correct = True
                    declaration[k] = v
            elif k == CellOptionEnum.TWEAKS:
                if isinstance(v, (tuple, list)):
                    correct = True
                    for x in v:
                        if not isinstance(x, (str, unicode)):
                            correct = False
                            break
                    v = tuple(v)
                elif isinstance(v, (str, unicode)):
                    correct = True
                    v = (v,)
                if correct:
                    declaration[k] = v
            elif k == CellOptionEnum.ON_CHANGE:
                name = function_name(v)
                if name:
                    correct = True
                    declaration[k] = v
            elif k == CellOptionEnum.TYPE:
                correct = True
                declaration[k] = type_mgr.TypeManager(v)
            elif k == CellOptionEnum.SET_FUN:
                name = function_name(v)
                if name:
                    correct = True
                    declaration[k] = v
            if not correct:
                raise exception.DefinitionException('Invalid cell parameter "%s = %s"' % (k, repr(v)))

        if (CellOptionEnum.TWEAKS in declaration) and not declaration.get(CellOptionEnum.SETTABLE, False):
            raise exception.DefinitionException('Cannot have tweaks on non-settable nodes')

        def internal(function):
            if not is_noarg_method(function):
                raise exception.DefinitionException('Cell cannot have parameters')
            function._NEPTUNE_declaration = declaration
            return function
        
        return internal
    
#######

class FunctionWrapper(object):
    
    __slots__ = ('_function', '_instance')
    
    def __init__(self, function, instance):
        self._function = function
        self._instance = instance
    
    def __call__(self):
        return self._function(self._instance)

#######

def add_link(mapping, source, target):
    if hasattr(target, 'set'):
        mapping.setdefault(source, []).append(target)

#######

class NodeDescriptor(object):
    
    __slots__ = ('_name', '_declaration')
    
    def __init__(self, name, declaration = None):
        self._name = name
        self._declaration = declaration
    
    def __get__(self, instance, owner):
        if instance:
            return instance.__dict__[self._name].get()
        else:
            raise AttributeError(self._name)

    def __set__(self, instance, value):
        if instance:
            instance.__dict__[self._name].set(value)
        else:
            raise AttributeError(self._name)
        
    def __delete__(self, instance):
        if instance:
            instance.__dict__[self._name].reset()
        else:
            raise AttributeError(self._name)

#######

class NodeView(object):

    def __init__(self, instance):
        self._NEPTUNE_instance = instance

    def __getattr__(self, name):
        try:
            target = self._NEPTUNE_instance.__dict__[name]
        except KeyError:
            raise AttributeError('Member "%s" doesn\'t exist in instance' % name)
        if not isinstance(target, node.BaseNode):
            raise AttributeError('Member "%s" isn\'t a node' % name)
        return target

#######

class Metaclass(type):
    
    def __new__(cls, name, bases, content):
        if name != 'Page':
            
            if not bases or (len(bases) > 1) or (bases[0] != Page):
                raise exception.DefinitionException('Base class is not Page')

            static_nodes   = {}
            dynamic_nodes  = {}
            settable_nodes = {}
            on_change_map  = {}
            tweaks_map     = {}
            type_map       = {}
            
            for (k, v) in content.items():
                if k in ('__init__',):
                    raise exception.DefinitionException("'%s' can't be redefined" % k)
                if isinstance(v, (staticmethod, classmethod)):
                    pass
                elif k.startswith('__'):
                    pass
                elif inspect.isfunction(v):
                    if hasattr(v, '_NEPTUNE_declaration'):
                        declaration = v._NEPTUNE_declaration
                        if declaration.get(CellOptionEnum.SETTABLE):
                            content[k] = NodeDescriptor(k, declaration)
                            settable_nodes[k] = v
                        else:
                            content[k] = NodeDescriptor(k, declaration)
                            dynamic_nodes[k] = v
                        if CellOptionEnum.ON_CHANGE in declaration:
                            on_change_map[k] = declaration[CellOptionEnum.ON_CHANGE]
                        if CellOptionEnum.TWEAKS in declaration:
                            tweaks_map[k] = declaration[CellOptionEnum.TWEAKS]
                        if CellOptionEnum.TYPE in declaration:
                            type_map[k] = declaration[CellOptionEnum.TYPE]
                else:
                    content[k] = NodeDescriptor(k)
                    static_nodes[k]= v
            
            for (k, v) in on_change_map.items():
                if v not in content:
                    raise exception.DefinitionException("Cell '%s' referred to on_change '%s' that hasn't been declared" % (k, v))
                if not callable(content[v]):
                    raise exception.DefinitionException("Cell '%s' referred to on_change '%s' which isn't callable" % (k, v))

            for (k, v) in tweaks_map.items():
                for k2 in v:
                    if k2 not in content:
                        raise exception.DefinitionException("Cell '%s' referred to a tweak '%s' that hasn't been declared" % (k, k2))
            
            content['_NEPTUNE_static_nodes']   = static_nodes
            content['_NEPTUNE_dynamic_nodes']  = dynamic_nodes
            content['_NEPTUNE_settable_nodes'] = settable_nodes
            content['_NEPTUNE_on_change_map']  = on_change_map
            content['_NEPTUNE_tweaks_map']     = tweaks_map
            content['_NEPTUNE_type_map']       = type_map
            
        return super(Metaclass, cls).__new__(cls, name, bases, content)

#######

class LinkSetter(object):
    
    __slots__ = ('_targets')
    
    def __init__(self, targets):
        self._targets = targets
    
    def __call__(self, value):
        for target in self._targets:
            target.set(value)

#######

class Shared(object):
    
    def __init__(self, value):
        self.value = value

#######

class Page(object):
    
    __metaclass__ = Metaclass

    def __init__(self, *args, **kwargs):
        super(Page, self).__init__()
        if args:
            raise exception.DefinitionException('All arguments in constructor must be named')
        
        new_change_map = dict((k, getattr(self, v)) for (k, v) in self._NEPTUNE_on_change_map.items())
        
        link_map = {}
        
        for (k, v) in self._NEPTUNE_static_nodes.items():
            if k in kwargs:
                value = kwargs[k]
            elif isinstance(v, Shared):
                value = v.value
            else:
                value = copy.deepcopy(v)
            if isinstance(value, node.BaseNode):
                value2 = value.get()
            else:
                value2 = value
            self.__dict__[k] = node.StaticNode(value    = value2,
                                               name     = k,
                                               owner    = self,
                                               listener = new_change_map.get(k))
            if isinstance(value, node.BaseNode):
                add_link(link_map, value, self.__dict__[k])
                add_link(link_map, self.__dict__[k], value)
    
        for (k, v) in self._NEPTUNE_dynamic_nodes.items():
            self.__dict__[k] = node.DynamicNode(function = FunctionWrapper(v, self),
                                                name     = k,
                                                owner    = self,
                                                type_    = self._NEPTUNE_type_map.get(k),
                                                listener = new_change_map.get(k))
        
        for (k, v) in self._NEPTUNE_settable_nodes.items():
            if k in kwargs:
                value = kwargs[k]
                if (value is not None) and (k in self._NEPTUNE_type_map):
                    self._NEPTUNE_type_map[k].check(value)
                if isinstance(value, node.BaseNode):
                    value2 = (value.get(),)
                else:
                    value2 = (copy.deepcopy(value),)
            else:
                value = None
                value2 = None
            self.__dict__[k] = node.SettableNode(function = FunctionWrapper(v, self),
                                                 value    = value2,
                                                 name     = k,
                                                 owner    = self,
                                                 type_    = self._NEPTUNE_type_map.get(k),
                                                 listener = new_change_map.get(k))
            if isinstance(value, node.BaseNode):
                add_link(link_map, value, self.__dict__[k])
                add_link(link_map, self.__dict__[k], value)
        
        for k in kwargs:
            if all([k not in self._NEPTUNE_static_nodes,
                    k not in self._NEPTUNE_settable_nodes]):
                raise exception.DefinitionException("Cell '%s' hasn't been declared" % k)
    
        for (k, v) in self._NEPTUNE_tweaks_map.items():
            for n in v:
                add_link(link_map, self.__dict__[n], self.__dict__[k])
        
        self.__dict__['_NEPTUNE_monitors'] = []
        for (k, v) in link_map.items():
            self._NEPTUNE_monitors.append(node.DynamicNode(function = k.get,
                                                           listener = LinkSetter(v)))
                    
        self.__dict__['_NEPTUNE_thread_id'] = thread.GLOBALS.thread_id
        
    def __invert__(self):
        return NodeView(self)

    def __setattr__(self, name, value):
        if name not in self.__dict__:
            raise AttributeError(name)
        super(Page, self).__setattr__(name, value)
